package anonymouschat.utils;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;

import javax.crypto.SecretKey;

import anonymouschat.client.Identity;
import anonymouschat.client.NickNameList;
import anonymouschat.exceptions.DecryptionException;
import anonymouschat.exceptions.InvalidTagException;
import anonymouschat.messages.securitymessages.AESEncryptedMessage;
import anonymouschat.security.KeySalt;
import anonymouschat.security.NameSaltPairs;
import anonymouschat.security.SecurityUtils;
import anonymouschat.security.SerializedObjectAndTag;

/**
 * Interactions between client and local file.
 * 
 * @author hb
 * 
 */
public class LocalFileOps {

	/**
	 * Loads the list of previous user names and the list of random salt and iv
	 * values used in creating their AESKeys (with the user's password)
	 * 
	 * @return
	 */
	public static NameSaltPairs getPreviousUserNamesAndSalt() {
		String fn = Constants.PREV_USERNAME_PASSWORD_FILE_NAME;
		NameSaltPairs nsp = null;
		try {
			nsp = (NameSaltPairs) LocalFileOps.readObject(fn);
		} catch (FileNotFoundException e) {
			Print.error("Previous Username and salt file -- " + fn
					+ " not found.");
			nsp = new NameSaltPairs();
		}

		return nsp;
	}

	/**
	 * Saves the list of previous user identities and their random salt values
	 * to the file system
	 * 
	 * @param tosave
	 */
	public static void writePreviousUsers(NameSaltPairs tosave) {
		String f = Constants.PREV_USERNAME_PASSWORD_FILE_NAME;
		LocalFileOps.write(tosave, f);
	}

	/**
	 * Get Private chatting history. Turn the List of Strings into one String.
	 * 
	 * @param identity
	 * @param dstUserName
	 * @return
	 * @throws ClassNotFoundException
	 */
	public static String getChatHist(Identity identity, String dstUserName)
			throws ClassNotFoundException, IOException, FileNotFoundException {
		String filename = Constants.CLIENT_FOLDER + "/"
				+ identity.getUserName() + Constants.CHAT_HIST_FILE_NAME_MIDDLE
				+ dstUserName;
		String chatHistory = "";
		try {
			chatHistory = (String) LocalFileOps.readMacThenEncObject(
					identity.getFileEncKey(), identity.getFileMacKey(),
					identity.getFileSalt(), filename);
		} catch (FileNotFoundException e) {
			throw e;
		} catch (Exception e) {
			Print.error("Cannot read chat history", e);
		}
		return chatHistory;
	}

	/**
	 * Append chat history file with a list of messages. If no existing history,
	 * create new file.
	 * 
	 * @param identity
	 * @param dstUserName
	 * @param string
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public static void appendChatHist(Identity identity, String dstUserName,
			String string) throws ClassNotFoundException {
		// read previous chat history first
		String chatHistory = "";
		String filename = Constants.CLIENT_FOLDER + "/"
				+ identity.getUserName() + Constants.CHAT_HIST_FILE_NAME_MIDDLE
				+ dstUserName;
		try {
			chatHistory = LocalFileOps.getChatHist(identity, dstUserName);
		} catch (FileNotFoundException fnf) {
			try {
				File file = new File(filename);
				file.createNewFile();
			} catch (IOException e) {
				Print.error("Cannont create file for chat log.", e);
				return;
			}
		} catch (IOException e1) {
			Print.error(
					"Failed to read chat history, so it could not be appended.",
					e1);
			return;
		}
		LocalFileOps.writeMacThenEncObject(chatHistory + string, identity,
				filename);
	}

	/**
	 * Append the local audit file.
	 * 
	 */
	public static void appendAuditFile(String description) {
		Date date = new Date();
		String oldFile = "";
		String newFile = "";
		try {
			oldFile = (String) readString(Constants.AUDIT_FILENAME);
			newFile = oldFile + date.toString() + "\t" + description + "\r\n";
		} catch (FileNotFoundException e) {
			Print.debug("Created a new audit file");
			File file = new File(Constants.AUDIT_FILENAME);
			try {
				file.createNewFile();
				newFile = date.toString() + "\t" + description + "\r\n";
			} catch (IOException e1) {
				Print.error("Cannot create a new audit file", e1);
			}
		}
		writeString(newFile, Constants.AUDIT_FILENAME);
	}

	/**
	 * Write Nickname List file using MAC-then-Enc
	 * 
	 * @param identity
	 * @param nickList
	 */
	public static void writeNickList(Identity id, NickNameList nickList)
			throws IOException {
		try {
			String filename = Constants.NICKLIST_FILE_NAME_PREFIX
					+ id.getUserName();
			writeMacThenEncObject(nickList, id, filename);
		} catch (Exception e) {
			Print.error("Trouble writing nick name list as file.", e);
		}
	}

	/**
	 * Write the object to the filesysten using the keys/salt associated with
	 * the identity
	 * 
	 * @param toSave
	 *            OBJECT TO SAVE
	 * @param id
	 *            identity saving the object
	 * @param filename
	 *            filename where saving
	 */
	public static void writeMacThenEncObject(Serializable toSave, Identity id,
			String filename) {
		writeMacThenEncObject(toSave, id.getFileEncKey(), id.getFileMacKey(),
				id.getFileSalt().getIv(), filename);
	}

	/**
	 * Write the object to the file as Enc( {toSave, Mac(toSave; macKey);
	 * encKey)
	 * 
	 * @param toSave
	 *            object to save
	 * @param encKey
	 *            key for encrypting the object after MAC-ing
	 * @param macKey
	 *            key for MAC-ing the object to be encrypted
	 * @param iv
	 *            encryption IV
	 * @param filename
	 *            file to save
	 */
	public static void writeMacThenEncObject(Serializable toSave,
			SecretKey encKey, SecretKey macKey, byte[] iv, String filename) {
		// Create an object which wraps <toSave, MAC(toSave)>
		SerializedObjectAndTag objAndTag = new SerializedObjectAndTag(toSave,
				macKey);
		// Encrypt the association
		AESEncryptedMessage enced = SecurityUtils.encAES(objAndTag, encKey, iv);
		LocalFileOps.write(enced, filename);
	}

	/**
	 * Get Nickname list from file. The file is saved in a MAC-then-Enc format,
	 * so this method decrypts the object, and then checks if it is valid
	 * 
	 * @param nickList
	 * @return
	 * @throws DecryptionException
	 */
	public static NickNameList getNickList(Identity id)
			throws DecryptionException {
		NickNameList nickList = null;
		String filename = Constants.NICKLIST_FILE_NAME_PREFIX
				+ id.getUserName();
		try {
			nickList = (NickNameList) LocalFileOps.readMacThenEncObject(
					id.getFileEncKey(), id.getFileMacKey(), id.getFileSalt(),
					filename);
		} catch (FileNotFoundException e) {
			Print.error("Nick name list file not found, creating new list");
		} catch (InvalidTagException e) {
			Print.error("Failed to verify the integrity of the nickname list",
					e);
		} finally {
			if (nickList == null)
				nickList = new NickNameList();
		}

		return nickList;
	}

	/**
	 * Write string to the file system under the name filename
	 * 
	 * @param writeme
	 * @param filename
	 */
	public static void writeString(String writeme, String filename) {
		FileWriter fstream = null;
		BufferedWriter out = null;
		try {
			// Create file
			fstream = new FileWriter(filename);
			out = new BufferedWriter(fstream);
			out.write(writeme);
		} catch (Exception e) {// Catch exception if any
			System.err.println("Error: " + e.getMessage());
		} finally {
			// Close the output stream
			try {
				if (out != null)
					out.close();
				if (fstream != null)
					fstream.close();
			} catch (IOException e) {
				Print.error("Failed to close streams correctly.", e);
			}
		}
	}

	/**
	 * Write the Serializable object to the file system under the name filename
	 * 
	 * @param writeme
	 * @param filename
	 */
	public static void write(Serializable writeme, String filename) {
		FileOutputStream fos = null;
		ObjectOutputStream out = null;
		try {
			fos = new FileOutputStream(new File(filename));
			out = new ObjectOutputStream(fos);
			out.writeObject(writeme);
		} catch (FileNotFoundException f) {
			Print.error("Could not find file " + filename + " write failed.", f);
		} catch (IOException e) {
			Print.error("Failed to write object to file -- " + filename, e);
		} finally {
			// close streams
			try {
				if (out != null)
					out.close();
				if (fos != null)
					fos.close();
			} catch (IOException e) {
				Print.error("Failed to close streams correctly.", e);
			}
		}
	}

	/**
	 * Read string from file
	 * 
	 * @param filename
	 * @return
	 * @throws FileNotFoundException
	 */
	public static String readString(String filename)
			throws FileNotFoundException {
		byte[] buffer = new byte[(int) new File(filename).length()];
		FileInputStream f;
		f = new FileInputStream(filename);
		try {
			f.read(buffer);
		} catch (IOException e) {
			Print.error("Cannot read audit file", e);
		} finally {
			try {
				if (f != null)
					f.close();
			} catch (IOException e) {
				Print.error("Failed to close file after write", e);
			}
		}
		return new String(buffer);
	}

	/**
	 * Read object from file
	 * 
	 * @param filename
	 * @return
	 * @throws FileNotFoundException
	 */
	public static Object readObject(String filename)
			throws FileNotFoundException {
		FileInputStream fis = null;
		ObjectInputStream ois = null;
		Object obj = null;
		try {
			fis = new FileInputStream(new File(filename));
			ois = new ObjectInputStream(fis);
			obj = ois.readObject();
		} catch (FileNotFoundException f) {
			throw f;
		} catch (IOException e) {
			Print.error("Cannon open file -- " + filename, e);
		} catch (ClassNotFoundException e) {
			Print.error("Cannot read object in from file.", e);
		} finally {
			try {
				if (ois != null)
					ois.close();
				if (fis != null)
					fis.close();
			} catch (IOException e) {
				Print.error("Failed to close streams input streams.", e);
			}
		}

		return obj;
	}

	/**
	 * Decrypt and read in the Identity object associated with the user Must
	 * provide the secret key (ie use Keys.createAESKey()) and must provide the
	 * salt used to encrypt the user's files
	 * 
	 * @param userName
	 * @param key
	 * @param salt
	 * @return
	 */
	public static Identity getIdentityFromFile(String userName, SecretKey key,
			SecretKey tagkey, KeySalt salt) {
		String filename = Constants.ID_FILE_NAME_PREFIX + userName;
		Identity loaded = null;
		try {
			loaded = (Identity) readMacThenEncObject(key, tagkey, salt,
					filename);
		} catch (FileNotFoundException f) {
			Print.error("File not found for past ID -- " + userName);
		} catch (InvalidTagException e) {
			Print.error(
					"Integerity of previous Identity file can't be verified.",
					e);
		} catch (Exception e) {
			Print.error("Problem loading previous Idenity from file system", e);
		}
		return loaded;
	}

	/**
	 * @param enckey
	 * @param tagkey
	 * @param salt
	 * @param filename
	 * @return
	 * @throws FileNotFoundException
	 * @throws InvalidTagException
	 * @throws DecryptionException
	 */
	public static Serializable readMacThenEncObject(SecretKey enckey,
			SecretKey tagkey, KeySalt salt, String filename)
			throws FileNotFoundException, InvalidTagException,
			DecryptionException {
		Serializable loaded;
		// Read in encrypted wrapper <=> Enc(id,Mac(id);id_filekey)
		AESEncryptedMessage enced = (AESEncryptedMessage) LocalFileOps
				.readObject(filename);
		// Decrypt the wrapper
		SerializedObjectAndTag idAndTag = (SerializedObjectAndTag) SecurityUtils
				.decAES(enced, enckey, salt.getIv());
		// Verify and get Id from wrapper
		loaded = idAndTag.getVerifiedObject(tagkey);
		return loaded;
	}

	/**
	 * Write user identity as an encrypted file
	 * 
	 * @param n
	 *            the identity to store
	 * @param key
	 *            the key used to encrypt the identity
	 * @param salt
	 */
	public static void writeUserId(Identity n, SecretKey key, KeySalt salt) {
		String fn = Constants.ID_FILE_NAME_PREFIX + n.getUserName();
		try {
			LocalFileOps.writeMacThenEncObject(n, n, fn);
		} catch (Exception e) {
			Print.error("Failed to encrypt Identity.", e);
		}
	}

	/**
	 * 
	 * @param svrName
	 *            the server name to use
	 * @return &ltString&gt of the sub-folder and file name
	 */
	public static String getSaltFileName(String svrName) {
		return Constants.SERVER_FOLDER + "/salt_" + svrName + ".dat";
	}

	/**
	 * 
	 * @param svrName
	 *            the server identity to look for
	 * @param isPublic
	 *            is the identity public or private
	 * @return
	 */
	public static boolean identityExists(String svrName) {
		File f = null;
		f = new File(LocalFileOps.getPrivateIdentityFileName(svrName));
		return f.exists() && f.canRead();// && g.exists() && g.canRead();
	}

	/**
	 * 
	 * @param svrName
	 *            the server name to use
	 * @return &ltString&gt of the sub-folder and file name
	 */
	public static String getPrivateIdentityFileName(String svrName) {
		return Constants.SERVER_FOLDER + "/private" + svrName + "Identity.dat";
	}

	/**
	 * 
	 * @param svrName
	 *            the server name to use
	 * @return &ltString&gt of the sub-folder and file name
	 */
	public static String getPublicIdentityFileName(String svrName) {
		return Constants.SERVER_FOLDER + "/public" + svrName + "Identity.dat";
	}
}
